using System;
using System.Text;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Hirham.OpenMI.Sdk.Backbone;
using Hirham.OpenMI.Sdk.Wrapper;
using Hirham.OpenMI.Sdk.Buffer;
using OpenMI.Standard;

namespace Hirham.OpenMI.Engine
{
  /// <summary>
  /// The HirhamEngine - implementing the convenience IEngine interface
  /// </summary>
  public class HirhamEngine : IEngine
  {

    // model information
    string modelID;

    // grid information
    BaseGrid hirhamGrid;

    IDataController HirHamDataAccess;

    // time, time step information
    System.TimeSpan timeStepLength;     // length of the "coupled" HIRHAM timeStep [seconds]
    DateTime simulationStart;
    DateTime currenttime;
    DateTime simulationEnd;
    //int timeStepNumber; // record numberInTheBuffer
    double valueFirstTimeStep;
    double timeStepValueIncrement;
    int currentTimeStepNumber;
    int offsetToUTC;

    // file, path information
    string filePath;

    // input and output exchange items
    List<InputExchangeItem> inputExchangeItems;
    List<OutputExchangeItem> outputExchangeItems;

    Dictionary<string, Dictionary<string, ScalarSet>> DataFromHirham = new Dictionary<string, Dictionary<string, ScalarSet>>();


    // status
    bool ahflIsUpdated; // is true when new data have been written to file
    bool tslIsUpdated; // is true when new data have been written to file

    internal HirhamLC hirhamLC;

    //This is used for running the code without connection to Hirham
    private HirhamEmulator PseudoHirham;

    public void Initialize(Hashtable properties)
    {

      ahflIsUpdated = false;
      tslIsUpdated = false;

      inputExchangeItems = new List<InputExchangeItem>();
      outputExchangeItems = new List<OutputExchangeItem>();

      #region OMIfile input values

      // it is assumed that the omi-file contains the following Keys:
      // ModelID         Name of the model run, e.g. HIRHAM HB2
      // StartTime       Start time for the coupled run, at run-time HIRHAM is already running,
      //                 when the model receives its first InputExchangeItems
      // EndTime         Final time step for the coupled run, HIRHAM will stop as indicated in its run.job script
      // OffsetToUTC     <> 0 if HIRHAM time (UTC) differs from the MIKE SHE time (local)
      // nx              No. of cells in the x-direction 
      // ny              No. of cells in the y-direction
      // ox              x-coordinate of the lower left grid cell 
      // oy              x-coordinate of the lower left grid cell 
      // CellSize        Length of a (quadratic) grid cell in meters
      // GridAngle       Rotation of the grid in degrees
      // TimeStepLength  Length of a time step in seconds
      // ValuesFirstTimeStep     The "no." of the first time step (usually 1) // fixme
      // TimeStepValueIncrement  Usually 1 // fixme
      // FilePath        Path to files ??

      modelID = (string)properties["ModelID"];
      simulationStart = DateTime.SpecifyKind(GetDateTimeArgument("StartTime", properties), DateTimeKind.Utc);
      currenttime = simulationStart;
      simulationEnd = GetDateTimeArgument("EndTime", properties);
      offsetToUTC = GetIntegerArgument("OffsetToUTC", properties);
      int nx = GetIntegerArgument("nx", properties);
      int ny = GetIntegerArgument("ny", properties);
      double oy = GetDoubleArgument("oy", properties);
      double ox = GetDoubleArgument("ox", properties);
      double cellSize = GetDoubleArgument("CellSize", properties);
      double gridAngle = GetDoubleArgument("GridAngle", properties);
      timeStepLength = System.TimeSpan.FromSeconds(GetDoubleArgument("TimeStepLength", properties));
      valueFirstTimeStep = GetDoubleArgument("ValuesFirstTimeStep", properties);
      timeStepValueIncrement = GetDoubleArgument("TimeStepValueIncrement", properties);
      filePath = Path.GetFullPath(GetStringArgument("FilePath", properties));

      HirHamDataAccess = new FileDataController(filePath);


      if (properties.ContainsKey("Pseudo"))
        if (bool.Parse(properties["Pseudo"].ToString()))
        {
          PseudoHirham = new HirhamEmulator(filePath, simulationStart.Add(timeStepLength), timeStepLength);
          modelID += "\nPSEUDO-RUN!!!";
        }

      hirhamGrid = new BaseGrid(ox, oy, cellSize, nx, ny, gridAngle);

      #endregion

      #region Create input and output exchange items

      // --- create a flux quantity (surface latent heat flux, ahfl)
      // W/m^2 = m^2*kg*s^-3*m^-2 = kg*s^-3
      Dimension fluxDimension = new Dimension();
      fluxDimension.SetPower(DimensionBase.Mass, 1);
      fluxDimension.SetPower(DimensionBase.Time, -3);
      Unit wattPrSqMeterUnit = new Unit("WattPrSqMeter", 1.0, 0.0, "Watt per square meter");

      InputExchangeItem ahflInputItem = new InputExchangeItem();
      ahflInputItem.ElementSet = hirhamGrid;
      ahflInputItem.Quantity = new Quantity(wattPrSqMeterUnit, "Surface latent heat flux", "ahfl", global::OpenMI.Standard.ValueType.Scalar, fluxDimension);
      inputExchangeItems.Add(ahflInputItem);

      // --- create a flux quantity (net SW surface radiation, srads)
      OutputExchangeItem sradsOutputItem = new OutputExchangeItem();
      sradsOutputItem.Quantity = new Quantity(wattPrSqMeterUnit, "Net SW surface radiation", "Net SW surface radiation (W/m2)", global::OpenMI.Standard.ValueType.Scalar, fluxDimension);
      sradsOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(sradsOutputItem);

      // --- create a flux quantity (global SW surface radiation, sradsu)
      OutputExchangeItem sradsuOutputItem = new OutputExchangeItem();
      sradsuOutputItem.Quantity = new Quantity(wattPrSqMeterUnit, "Global SW surface radiation", "Global SW surface radiation (W/m2)", global::OpenMI.Standard.ValueType.Scalar, fluxDimension);
      sradsuOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(sradsuOutputItem);

      // --- create a flux quantity (net LW surface radiation, trads)
      OutputExchangeItem tradsOutputItem = new OutputExchangeItem();
      tradsOutputItem.Quantity = new Quantity(wattPrSqMeterUnit, "Net LW surface radiation", "Net LW surface radiation (W/m2)", global::OpenMI.Standard.ValueType.Scalar, fluxDimension); ;
      tradsOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(tradsOutputItem);

      // --- create a temperature quantity in Kelvin (surface temperature, tsl)
      // K
      Dimension temperatureDimension = new Dimension();
      temperatureDimension.SetPower(DimensionBase.Temperature, 1);
      Unit temperatureUnit = new Unit("Kelvin", 1.0, -273.15, "Kelvin");

      InputExchangeItem tslInputItem = new InputExchangeItem();
      tslInputItem.ElementSet = hirhamGrid;
      tslInputItem.Quantity = new Quantity(temperatureUnit, "Surface temperature", "tsl", global::OpenMI.Standard.ValueType.Scalar, temperatureDimension);
      inputExchangeItems.Add(tslInputItem);


      // --- create a temperature quantity in Kelvin (2-m temperature, t2m)
      OutputExchangeItem t2mOutputItem = new OutputExchangeItem();
      t2mOutputItem.Quantity = new Quantity(temperatureUnit, "2-m temperature", "2-m temperature (K)", global::OpenMI.Standard.ValueType.Scalar, temperatureDimension);
      t2mOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(t2mOutputItem);

      // --- create a wind speed quantity (10-m wind speed, wind10)
      // m*s^2
      Dimension windspeedDimension = new Dimension();
      windspeedDimension.SetPower(DimensionBase.Length, 1);
      windspeedDimension.SetPower(DimensionBase.Time, -2);
      Unit windspeedUnit = new Unit("MeterPrSecond", 1.0, 0.0, "meter per second");

      OutputExchangeItem wind10OutputItem = new OutputExchangeItem();
      wind10OutputItem.Quantity = new Quantity(windspeedUnit, "10-m wind speed", "10-m wind speed (m/s)", global::OpenMI.Standard.ValueType.Scalar, windspeedDimension);
      wind10OutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(wind10OutputItem);

      // --- create a total precipitation rate quantity (precip = aprl + aprc)
      // mm*hour^-1
      Dimension precipDimension = new Dimension();
      precipDimension.SetPower(DimensionBase.Length, 1);
      precipDimension.SetPower(DimensionBase.Time, -1);
      Unit precipUnit = new Unit("MMPerHour", 2.778e-7, 0.0, "millimetres per hour");

      OutputExchangeItem precipOutputItem = new OutputExchangeItem();
      precipOutputItem.Quantity = new Quantity(precipUnit, "total precipation rate", "Total precipitation (mm/h)", global::OpenMI.Standard.ValueType.Scalar, precipDimension);
      precipOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(precipOutputItem);

      // --- create a surface pressure quantity in Pa (ps)
      // m^-1*kg*s^-2
      Dimension pressureDimension = new Dimension();
      pressureDimension.SetPower(DimensionBase.Length, -1);
      pressureDimension.SetPower(DimensionBase.Mass, 1);
      pressureDimension.SetPower(DimensionBase.Time, -2);
      Unit pressureUnit = new Unit("Pascal", 1.0, 0.0, "Pascal");

      OutputExchangeItem psOutputItem = new OutputExchangeItem();
      psOutputItem.Quantity = new Quantity(pressureUnit, "surface pressure of air", "Surface pressure (Pa)", global::OpenMI.Standard.ValueType.Scalar, pressureDimension);
      psOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(psOutputItem);

      // --- create a dimensionless 2-m relative humidity quantity (rh2m)
      // dimensionless (percent)
      Dimension no_dimensionDimension = new Dimension();
      Unit no_dimensionUnit = new Unit("Dimensionless", 1.0, 0.0, "Dimensionless");
      OutputExchangeItem rh2mOutputItem = new OutputExchangeItem();
      rh2mOutputItem.Quantity = new Quantity(no_dimensionUnit, "2-m relative humidity (0-1)", "2-m relative humidity", global::OpenMI.Standard.ValueType.Scalar, no_dimensionDimension);
      rh2mOutputItem.ElementSet = hirhamGrid;
      outputExchangeItems.Add(rh2mOutputItem);

      #endregion

      //This step is to have initial values
      SetInitialValues(psOutputItem, 101300);
      SetInitialValues(rh2mOutputItem, 0.75);
      SetInitialValues(t2mOutputItem, 285);
      SetInitialValues(precipOutputItem, 0);
      SetInitialValues(wind10OutputItem, 0);
      SetInitialValues(tradsOutputItem,75);
      SetInitialValues(sradsOutputItem, 75);
      SetInitialValues(sradsuOutputItem, 0);
    }


    public bool PerformTimeStep()
    {
      //Start a timer
      System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
      sw.Start();

      //Get the data from the dataaccess
      SendEvent("Getting data from Hirham. Waiting for 'hirham_data_ready.txt'");

      while(!ReadDataFromHirham())
        System.Threading.Thread.Sleep(100);



      //Stop the timer
      sw.Stop();
      //Send event with time information
      SendEvent("Call to performtimestep in hirham engine. Computation time [s]: " + sw.Elapsed.TotalSeconds);

      return true;
    }

    private void SendEvent(string Description)
    {
      Event e = new Event(EventType.Informative);
      e.SimulationTime = (TimeStamp)GetCurrentTime();
      e.Description = Description;
      e.Sender = hirhamLC;
      hirhamLC.SendEvent(e);

    }

    private int LastTimeStepError=-1;

    private bool ReadDataFromHirham()
    {
      bool success = false;
      string datastring="";
      try
      {
        datastring = HirHamDataAccess.GetHirhamData(out currenttime);

        //Now parse the data
        string[] dataArray = datastring.Split(new string[] { "\n" }, StringSplitOptions.None).Where(l => l.Trim() != "").ToArray();
        int k = 0;
        while (k < dataArray.Count() & dataArray.Count()-k > hirhamGrid.Ny)
        {
          
          string QuantityID = QuantityIDFromHeadLine(dataArray[k]);
          k++;
          Dictionary<string, ScalarSet> quantityset;

          if (!DataFromHirham.TryGetValue(QuantityID, out quantityset)) // Try to get data and create new if the quantity does not exist
          {
            quantityset = new Dictionary<string, ScalarSet>();
            quantityset.Add(hirhamGrid.ID, new ScalarSet());
            DataFromHirham.Add(QuantityID, quantityset);
          }

          var scalarset = quantityset[hirhamGrid.ID];
          if (scalarset.data==null)
            scalarset.data = new double[hirhamGrid.NumberOfCells];

          int localk = 0;
          for (int i = 0; i < hirhamGrid.Ny; i++)
          {
            string[] fileValues = dataArray[k].Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
            if (fileValues.Count() < hirhamGrid.Nx)
              SendEvent("Too few data in Hirham.out. Using last timestep. Quantity: " + QuantityID); 

            for (int j = 0; j < Math.Min(fileValues.Count(), hirhamGrid.Nx); j++)
            {
              scalarset.data[localk] = Double.Parse(fileValues[j]);
              localk++;
            }
            k++;
          }
        }

        currentTimeStepNumber++;
        success = true;
      }
      catch(Exception ex)
      {
        if (LastTimeStepError != currentTimeStepNumber)
        {
          LastTimeStepError = currentTimeStepNumber;

          using (StreamWriter sw = new StreamWriter("HirhamError.Log", true))
          {
            sw.WriteLine("Error reading hirham data. Time:" + DateTime.Now);
            sw.WriteLine("Hirham.out has been read like this:");
            sw.WriteLine(datastring);
            sw.WriteLine();

            Event e = new Event(EventType.Informative);
            e.SimulationTime = (TimeStamp)GetCurrentTime();
            e.Description = ("Error getting data from Hirham. Keep trying. Check that Hirham_data_ready.txt, Hirham_data_ready_timestep.txt and Hirham.out are there in correct format");
            e.Sender = hirhamLC;
            hirhamLC.SendEvent(e);
          }
        }

      }

      return success;


    }



    public ITime GetCurrentTime()
    {
      return new Hirham.OpenMI.Sdk.Backbone.TimeStamp(currenttime);
    }

    public ITime GetInputTime(string QuantityID, string ElementSetID)
    {
      Hirham.OpenMI.Sdk.Backbone.TimeStamp inputStart = new TimeStamp(currenttime);
      Hirham.OpenMI.Sdk.Backbone.TimeStamp inputEnd = new TimeStamp(currenttime.Add(timeStepLength));
      return new Hirham.OpenMI.Sdk.Backbone.TimeSpan(inputStart, inputEnd);
    }

    public ITimeStamp GetEarliestNeededTime()
    {
      return (ITimeStamp)GetCurrentTime();
    }


    public void SetValues(string quantityID, string elementSetID, IValueSet values)
    {

      StringBuilder sb = new StringBuilder();
      sb.AppendLine("Modelvalues " + currenttime.ToString());
      int k = 0;
      for (int i = 0; i < hirhamGrid.Ny; i++)
      {
        for (int j = 0; j < hirhamGrid.Nx; j++)
        {
          sb.Append(((ScalarSet)values).data[k].ToString("0.00000E+00") + "   ");
          k++;
        }
        sb.AppendLine();
      }

      sb.AppendLine("Mapping Matrix");
      k = 0;
      for (int i = 0; i < hirhamGrid.Ny; i++)
      {
        for (int j = 0; j < hirhamGrid.Nx; j++)
        {
          sb.Append(hirhamLC.mappingmatrix[k].ToString("0.00000E+00") + "   ");
          k++;
        }
        sb.AppendLine();
      }

      Event e = new Event(EventType.Informative);
      e.SimulationTime = (TimeStamp)GetCurrentTime();
      e.Description = ("Setting data to Hirham. Waiting for 'openmi_data_ready.txt' to be deleted");
      e.Sender = hirhamLC;
      hirhamLC.SendEvent(e);

      HirHamDataAccess.SetHirhamData(sb.ToString(), quantityID);

      if (elementSetID == hirhamGrid.ID && quantityID == "ahfl")
      {
        ahflIsUpdated = true;
      }
      else if (elementSetID == hirhamGrid.ID && quantityID == "tsl")
      {
        tslIsUpdated = true;
      }

      if (tslIsUpdated && ahflIsUpdated)
      {
        e.SimulationTime = (TimeStamp)GetCurrentTime();
        e.Description = ("No more output. Writing 'openmi_data_ready.txt'");
        e.Sender = hirhamLC;
        hirhamLC.SendEvent(e);


        HirHamDataAccess.NoMoreOutputInTimestep(currentTimeStepNumber, currenttime);
        tslIsUpdated = false;
        ahflIsUpdated = false;
      }
    }

    public IValueSet GetValues(string QuantityID, string ElementSetID)
    {

      Dictionary<string, ScalarSet> ValuesToReturn;
      if (!DataFromHirham.TryGetValue(QuantityID, out ValuesToReturn))
        throw new Exception("Unknown QuantityId in GetValues: " + QuantityID);
      else
      {
        ScalarSet sc;
        if (!ValuesToReturn.TryGetValue(ElementSetID, out sc))
          throw new Exception("Unknown ElementSetID in GetValues: " + ElementSetID);
        else
          return sc;
      }
    }

    public double GetMissingValueDefinition()
    {
      return (-99999.99);
    }

    public string GetComponentID()
    {
      return ("HIRHAMLinkableComponentEngineID");
    }

    public string GetComponentDescription()
    {
      return ("HIRHAMLinkableComponentEngineDescription");
    }

    public string GetModelID()
    {
      return (modelID);
    }

    public string GetModelDescription()
    {
      return ("DMI-HIRHAM5 Regional Climate Model (RCM)");
    }

    public ITimeSpan GetTimeHorizon()
    {
      TimeStamp startTime = new TimeStamp(Hirham.OpenMI.Sdk.DevelopmentSupport.CalendarConverter.Gregorian2ModifiedJulian(simulationStart));
      TimeStamp endTime = new TimeStamp(Hirham.OpenMI.Sdk.DevelopmentSupport.CalendarConverter.Gregorian2ModifiedJulian(simulationEnd));
      Hirham.OpenMI.Sdk.Backbone.TimeSpan timeHorizon = new Hirham.OpenMI.Sdk.Backbone.TimeSpan(startTime, endTime);
      return timeHorizon;
    }

    public int GetInputExchangeItemCount()
    {
      return inputExchangeItems.Count;
    }

    public InputExchangeItem GetInputExchangeItem(int exchangeItemIndex)
    {
      return (InputExchangeItem)inputExchangeItems[exchangeItemIndex];
    }

    public int GetOutputExchangeItemCount()
    {
      return outputExchangeItems.Count;
    }

    public OutputExchangeItem GetOutputExchangeItem(int exchangeItemIndex)
    {
      return (OutputExchangeItem)outputExchangeItems[exchangeItemIndex];
    }

    public void Finish()
    {
    }

    public void Dispose()
    {
    }


    #region Private methods

    private void SetInitialValues(OutputExchangeItem oei, double initialvalue)
    {

      Dictionary<string, ScalarSet> initialdata = new Dictionary<string, ScalarSet>();

      ScalarSet sc = new ScalarSet(new double[oei.ElementSet.ElementCount]);

      for (int i = 0; i < sc.data.Count(); i++)
        sc.data[i] = initialvalue;
      initialdata.Add(oei.ElementSet.ID, sc);

      DataFromHirham.Add(oei.Quantity.ID, initialdata);
    }


    private string QuantityIDFromHeadLine(string HeadLine)
    {
      HeadLine = HeadLine.Trim().Substring(2).Trim(); //Removes the first characters + any spaces in the end

      //Removes the date
      for (int i = 0; i < 6; i++)
      {
        HeadLine = HeadLine.Remove(HeadLine.LastIndexOf(" "));
      }

      return HeadLine.Trim();
    }

    private string GetStringArgument(string keyName, System.Collections.Hashtable properties)
    {
      string val;
      if (properties.ContainsKey(keyName))
      {
        val = properties[keyName].ToString();
      }
      else
      {
        throw new Exception("missing argument \"" + keyName + "\" in OMI file");
      }

      return val;
    }

    private double GetDoubleArgument(string keyName, System.Collections.Hashtable properties)
    {
      string str = GetStringArgument(keyName, properties);
      double x;
      try
      {
        x = Convert.ToDouble(str);
      }
      catch
      {
        throw new Exception("Failed to convert string in OMI file to double. Error occured for argument key= \"" + keyName + "\" and value= \"" + str);
      }

      return x;
    }

    private int GetIntegerArgument(string keyName, System.Collections.Hashtable properties)
    {
      string str = GetStringArgument(keyName, properties);
      int x;
      try
      {
        x = Convert.ToInt32(str);
      }
      catch
      {
        throw new Exception("Failed to convert string in OMI file to integer. Error occured for argument key= \"" + keyName + "\" and value= \"" + str);
      }

      return x;
    }

    private bool GetBoolianArgument(string keyName, System.Collections.Hashtable properties)
    {
      string str = GetStringArgument(keyName, properties);
      bool x;
      try
      {
        x = Convert.ToBoolean(str);
      }
      catch
      {
        throw new Exception("Failed to convert string in OMI file to boolian. Error occured for argument key= \"" + keyName + "\" and value= \"" + str);
      }

      return x;
    }

    private DateTime GetDateTimeArgument(string keyName, System.Collections.Hashtable properties)
    {
      string timeString = GetStringArgument(keyName, properties);
      char[] separator = new char[1] { ',' };
      DateTime dateTime;
      try
      {
        string[] datetimeString = timeString.Split(separator);
        dateTime = new DateTime(Convert.ToInt32(datetimeString[0]), Convert.ToInt32(datetimeString[1]), Convert.ToInt32(datetimeString[2]), Convert.ToInt32(datetimeString[3]), Convert.ToInt32(datetimeString[4]), Convert.ToInt32(datetimeString[5]));
      }
      catch
      {
        throw new Exception("Failed to convert string in OMI file to DateTime. Error occured for argument key= \"" + keyName + "\" and value= \"" + timeString);
      }
      return dateTime;
    }

    private bool GetHirhamTime(string filename, out DateTime currentHirhamTime)
    {
      string firstLine;
      try
      {
        using (StreamReader reader = new StreamReader(filename))
        {
          firstLine = reader.ReadLine();
          firstLine = firstLine.Trim().Substring(2).Trim(); //Removes the first characters + any spaces in the end

          // Extract the date (the format is "N" "HIRHAM" "write" "at" "(now:)" "YYYY" "MM" "DD" "HH" "MM" "SEC"  
          // i.e. the date and time is stored at array indices 5-10.
          string[] vstr = firstLine.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
          currentHirhamTime = new DateTime(Convert.ToInt32(vstr[5]), Convert.ToInt32(vstr[6]), Convert.ToInt32(vstr[7]), Convert.ToInt32(vstr[8]), Convert.ToInt32(vstr[9]), Convert.ToInt32(vstr[10]));
        }

        // convert to local time from HIRHAM/UTC
        currentHirhamTime = currentHirhamTime.AddHours(offsetToUTC);
        return true;
      }
      catch (Exception e)
      {
        currentHirhamTime = DateTime.MinValue;
        return false;
      }

    }

    private void WriteDataToHirham(string quantityid, ScalarSet values, double[] mappingMatrixValues)
    {

    }

    #endregion
  }
}